Navigation Models Overview
Flutter provides two navigation approaches, each suited for different use cases.
Imperative API (Navigator)
Simple push/pop stack-based navigation using Navigator.push, Navigator.pop, and named routes via Navigator.pushNamed. Works well for most apps and quick flows.
Declarative API (Router)
More control and integrates with browser URLs and deep linking. Use when building web apps or complex routing logic (URL-driven state).
Choose Navigator for most mobile apps; adopt Router when you need URL synchronization, nested route parsing, or advanced deep-link handling.
Basic Imperative Navigation with Navigator
The Navigator API provides straightforward methods for moving between screens.
Navigation Methods
- Push: Push a new route (screen) onto the navigation stack using
Navigator.push(context, MaterialPageRoute(builder: (_) => NewPage())). - Pop: Pop the current route with
Navigator.pop(context)or by using system back button. - Replace: Use
Navigator.pushReplacementto replace the current route orNavigator.pushAndRemoveUntilto clear stacks.
Example Patterns
Push:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsPage(item: item)),
);
Pop:
Navigator.pop(context);
Replace:
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomePage()),
);
Named Routes (Centralized)
Named routes provide a centralized way to manage navigation throughout your app.
Setting Up Named Routes
Define routes in MaterialApp(routes: {...}) or use onGenerateRoute for dynamic routing. Named routes improve consistency and make deep linking easier.
Pattern
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/details': (context) => DetailsPage(),
},
);
Navigate:
Navigator.pushNamed(context, '/details', arguments: {'id': 42});
Handle arguments in target:
final args = ModalRoute.of(context)!.settings.arguments as Map;
final id = args['id'];
Guideline: Use onGenerateRoute for argument validation and to avoid brittle casting at the call site.
Passing Data and Receiving Results
Flutter provides flexible ways to pass data between screens and receive results.
Passing Data
Pass data via constructors when pushing:
Navigator.push(context, MaterialPageRoute(builder: (_) => EditPage(item: item)));
Receiving Results
Return results using Navigator.pop(context, result) and await the push call:
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => FormPage())
);
if (result != null) { /* handle result */ }
Use cases: Form submission, selection dialogs, and confirmation screens.
Route Lifecycle and State Preservation
Understanding route lifecycle helps you manage state and resources effectively.
State Preservation
- When pushing new routes, previous route states are preserved by default (widgets remain in the tree but may be inactive).
- For heavy stateful screens with controllers, ensure proper disposal in
dispose()and consider caching strategies if route recreation is expensive. - Use
PageStorageandPageStorageKeyto preserve scroll positions and basic widget state across navigation.
Example: Preserve ListView Scroll Position
ListView.builder(
key: PageStorageKey('courseList'),
controller: _controller,
itemCount: ...
)
Nested Navigation and Independent Stacks
Complex apps often require independent navigation stacks for different sections.
Useful Pattern
BottomNavigationBar with independent navigation stacks per tab so users can switch tabs and resume interaction where they left off. Implement with an IndexedStack to keep tab screens alive and a Navigator per tab to manage its own stack.
Pattern Outline
- Top-level Scaffold with BottomNavigationBar
- Body: IndexedStack of Navigator widgets, each with its own
onGenerateRouteandNavigatorKey
Key Points
- Use a
GlobalKey<NavigatorState>per tab to push/pop within that tab. - Manage back button behavior: if active tab's Navigator can pop, pop it; otherwise, exit app or switch to default tab.
Drawer and Modal Routes
Drawers and modals provide alternative navigation patterns.
Drawer Navigation
Drawer navigation typically replaces or pushes routes; choose consistent behavior to avoid surprising users.
Modal Routes
Modal routes (showDialog, showModalBottomSheet) overlay content; they return results via Navigator.pop.
Example: showDialog Returns Result
final confirmed = await showDialog(
context: context,
builder: (_) => ConfirmDialog()
);
if (confirmed == true) { /* user confirmed */ }
Declarative Routing with Router (Brief Guide)
The Router API provides advanced routing capabilities for complex applications.
When to Use Router
Use Router, RouterDelegate, and RouteInformationParser when building web-first apps or complex URL-driven flows. High-level idea: parse incoming URL into a configuration object, let RouterDelegate build a stack of Pages from that configuration, and update browser history when configuration changes.
When to Pick Router
- Your app must reflect URL path segments and query parameters.
- You need deep linking integrated with complex nested navigation or restoration via URLs.
Note: Router has a steeper learning curve but offers full control over URL to UI mapping and back stack.
Deep Linking and App Links
Deep links allow external sources to open your app to specific screens.
Platform Integrations
- Android: Configure intent filters in AndroidManifest.xml (app links for verified domains).
- iOS: Configure Universal Links with Associated Domains and URL types.
- Flutter: Use packages like
uni_links,receive_sharing_intent, orflutter_branch_iofor richer integrations.
Flow
- App receives incoming URI string via platform channel or package API.
- Parse the URI into route name and arguments.
- Navigate to the appropriate screen, considering whether the app was cold-started or already running.
Example Deep Link Handler (Conceptual)
void handleIncomingUri(Uri uri) {
if (uri.pathSegments.isEmpty) return;
switch (uri.pathSegments.first) {
case 'course':
final id = uri.queryParameters['id'];
Navigator.pushNamed(context, '/course', arguments: {'id': id});
break;
}
}
Guideline: Support both initial link handling (on app start) and link stream (while app running).
Handling Edge Cases and Security
Proper error handling and security are essential for robust navigation.
Best Practices
- Validate all deep-link parameters server-side where necessary; do not trust client-provided IDs for sensitive operations.
- Gracefully handle missing or invalid arguments by showing fallback screens or friendly error messages.
- Avoid pushing duplicate routes for the same content; deduplicate by checking existing route stack or using
pushReplacementwhen appropriate.
Testing Navigation
Testing navigation ensures your app's routing works correctly.
Widget Testing
- Write widget tests that pump the widget and use
tester.tapandtester.pumpAndSettle()to assert navigation outcomes. - For Router-based apps, test
RouteInformationParserandRouterDelegatein isolation by providing fake route information and verifying built pages.
Example Widget Test Pattern
Pump the app widget, tap navigation button, await pumpAndSettle, assert new screen text exists using find.text.
Best Practices and Organization
Organizing navigation code properly makes maintenance easier.
Best Practices
- Centralize route names and argument contracts in a single file or enum to avoid string typos.
- Keep navigation triggers in UI layers and navigation logic in coordinators/services when apps grow.
- Prefer passing strongly-typed objects in constructors rather than raw maps when possible; define a
RouteArgumentsmodel to reduce runtime casting errors. - Document route contracts (expected arguments and types) for team clarity.
Suggested File Layout
lib/routes.dart— route name constants and helper navigation functionslib/navigation/— navigator keys, coordinators, and tab navigation helperslib/screens/— individual screen widgets (each screen owns its own argument parsing)
Exercises
Practice what you've learned with these exercises:
1. Simple push/pop
Implement HomePage with a list of items. On tap, push DetailsPage passing the tapped item. On DetailsPage, include a Delete button that pops with a boolean result; on HomePage, remove the item if result is true.
2. Named routes and arguments
Configure named routes and navigate via Navigator.pushNamed, passing a typed CourseArgs object. On the target page, use ModalRoute to extract and display fields.
3. BottomNavigation with independent stacks
Build a 3-tab app (Home, Search, Profile) where each tab maintains its own navigation stack. Implement back button handling so back pops the active tab's stack first.
4. Deep-link simulation
Simulate receiving a deep link URI that navigates to a course detail screen. Handle both cold start (app not running) and warm start (app in background) scenarios using a package like uni_links (conceptually if environment constraints prevent full integration).
5. Router basics (optional advanced)
Implement a simple Router-based app where URL paths / and /profile/:id map to HomePage and ProfilePage respectively. Verify resizing the browser updates the displayed route if applicable.
Session Assignment
Complete Exercises 1–3 with well-typed argument models and include unit/widget tests for navigation actions. Provide a short README describing how you organized routes, why you chose Navigator vs Router, and how you handled state restoration for each tab.